// Copyright 2010, Vanya Davidenko.
// Используемая кодировка: UTF-8.

#ifndef SCRW_EXTRACTOR_HPP_
#define SCRW_EXTRACTOR_HPP_

#include <algorithm>
#include <cstdlib>
#include <cstdio>


#include <iostream>
#include <fstream>

#include <unicode/regex.h>
#include <unicode/ucsdet.h>

#include "html_text_extractor.h"
#include "html_url_extractor.h"
#include "manager.h"
#include "common.h"


namespace scrw
{

/** Непотокобезопасный объект, выполняющий задания разбора документов,
  * получаемые от создавшего его менеджера. Под разбором подразумевается
  * выделение текста из plain text, html, xhtml и ссылок из html, xhtml,
  * rss/atom. Тип документа определяется по переданному mime-типу.
  * В случае, если в задании он не указан, то производится попытка найти
  * тег <meta> в html-документе и в случае неудачи — менеджеру отправляется
  * сообщение об ошибке и обработка завершается. Если в задании не указана
  * кодировка, то производится попытка найти тег <meta> в html-документа и в
  * случае неудачи, производится попытка аналитического получения кодировки.
  * Если с большой вероятностью кодировку определить не удалось считается, что
  * используется "iso-8859-1".
  *
  * При выделении ссылок сначала производится поиск базового адреса (тег base),
  * и если он не найден — используется домен. Таким образом, выделенные
  * ссылки всегда являются абсолютными адресами. В результат не попадают ссылки,
  * содержащие javascript. Через менеджер настраивается влияние атрибута
  * rel="nofollow" на тег <a> — такие ссылки или пропускаются или добавляются.
  *
  * При выделении текста, все удаляются все теги, комментарии, а так же все
  * скрипты, стили и служебные сообщения (например, между тегами <embed>).
  * Содержимое cdata сохраняется.
  * Возможна замена тегов на заданные строки (настраивается через менеджер).
  *
  * Создание объекта осуществляется методом Manager::GetExtractor().
  *
  * Класс не поддерживает присваивание, так как агегирует объекты с данным
  * ограничением, однако, разрешено копирование. Таким образом, возможно
  * создание объекта в куче и использование его через указатель.
  *
  * Под непотокобезопасностью подразумевается запрет на совместное использование
  * одного объекта несколькими потоками. Разрешено создание нескольких объектов
  * для одновременной работы в нескольких потоках.
  *
  * Пример использования:
  * <code>
  * Manager m(...);
  *
  * // Создание в нескольких потоках параллельно работающих объектов.
  * Manager::Extractor e = m.GetExtractor();
  *
  * // Создание объекта в куче (не используется в примере).
  * Manager::Extractor* pe = new Manager::Extractor(m.GetExtractor());
  *
  * // Выполнение заданий пока они есть. Если метод вернул false — задания
  * // закончились, но возможно появятся, если есть еще страницы,
  * // которые не были загружены.
  * while ( e.TryDoOneJob() ) continue;
  * </code> */
class Manager::Extractor
{
  friend class Manager;  // только менеджер может создавать данный объект.
  public:

    Extractor(const Extractor& rhv)
      : manager_(rhv.manager_),
        url_extractor_(rhv.url_extractor_),
        extract_text_(rhv.extract_text_),
        text_extractor_(rhv.text_extractor_),
        err_(U_ZERO_ERROR),
        meta_content_type_extractor_(
            meta_content_type_extractor_.pattern().pattern(),
            meta_content_type_extractor_.pattern().flags(),
            err_),
        url_converter_(rhv.url_converter_)
    {
        VERIFY(U_SUCCESS(err_) == TRUE);
    }



    /** Попытка получить и выполнить новое задание менеджера.
      * @retval true получено 1 задание, результат отправлен менеджеру
      * @retval false очередь заданий менеджера пуста.
      * Метод не потокобезопасен и не генерирует исключения. */
    bool TryDoOneJob()
    {
        // Всегда забираем задачу, так как это единственный способ атомарно
        // проверить их наличие (через возврат пустой задачи) и получить новую.
        Manager::ExtractorJob job = manager_->PopExtractorJob();
        if ( job.depth == -1 )
            return false;

        // На всякий случай.
        job.urls.clear();
        job.text.remove();

        if ( job.url.isBogus() || job.parent_url.isBogus() )
        {
            manager_->PushFailedExtractorJob(job, "Url is bogus");
            return true;
        }

        FillEmptyFieldsInJob(&job);

        if ( job.mime_type.empty() )
        {
            manager_->PushFailedExtractorJob(job, "Unable to get mime type");
            return true;
        }

        if ( job.charset.empty() )
            job.charset = "iso-8859-1";  // латиница

        // Такое написание редко встречается в интернете, но icu о нем не знает.
        else if ( job.charset == "win-1251" )
            job.charset = "windows-1251";


        if ( job.mime_type == "text/plain" )
        {
            if ( extract_text_ )
            {
                job.text = ConvertDataToUnicode(job);
                if ( job.text.isBogus() )
                {
                    manager_->PushFailedExtractorJob(
                        job, "Unable to convert data to unicode");
                    return true;
                }
            }

            manager_->PushExtractorJobResult(job);
        }

        // Из академического интереса ссылки и текст извлекаются 2-мя разными
        // способами:
        //  - регулярными выражениями (просто, но медленно и корректно для
        //    подмножества html — не учитываются, например, незакрытые кавычки
        //    или комментарии внутри тегов)
        //  - конечным автоматом (сложная реализация, но очень быстро)
        else if ( job.mime_type == "text/html" ||
                  job.mime_type == "application/xhtml+xml" )
        {
            const icu::UnicodeString u_data = ConvertDataToUnicode(job);
            if ( u_data.isBogus() )
            {
                manager_->PushFailedExtractorJob(
                    job, "Unable to convert data to unicode");
                return true;
            }

            job.urls = url_extractor_.Extract(job.url, u_data);
            ConvertUrlsToUnicode(&job);

            if ( extract_text_ )
                job.text = text_extractor_.Extract(u_data);

            manager_->PushExtractorJobResult(job);
        }

        else if ( job.mime_type.find("xml") != MimeType::npos )
        {
            // Эти 2 условия нельзя объединять через &&
            // иначе получится другая семантика.
            // В случае ошибки ExtractFromXml сам пошлет сообщение об ошибке.
            if ( ExtractFromXml(&job) )
            {
                ConvertUrlsToUnicode(&job);
                manager_->PushExtractorJobResult(job);
            }
        }

        else if ( extract_text_ )
        {
            manager_->PushFailedExtractorJob(job, "Unsupported mime type");
        }

        else 
        {
            manager_->PushExtractorJobResult(job);
        }

        return true;
   }


  private:
    /** Присваивание запрещено. */
    Extractor& operator=(const Extractor&);

    Extractor(Manager* manager,
              const bool grab_nofollow, const bool extract_text)
        : manager_(manager),
          url_extractor_(grab_nofollow),
          extract_text_(extract_text),
          err_(U_ZERO_ERROR),

          // Не будет работать, если атрибуты переставлены местами, что не было
          // встречено ни разу за время тестирования можно поставить OR ('|'),
          // но тогда это сильно увеличит время обработки каждого файла.
          meta_content_type_extractor_(
              "(?i:                                                      "
              "    <meta [^>]*?                                          "
              "        http-equiv \\s*+ = \\s*+ ['\"]?Content-Type['\"]? "
              "        [^>]*?                                            "
              "        content \\s*+ =\\s*+                              "
              "        (?i:                                              "
              "            '([^']++)'                                    "
              "            |                                             "
              "            \"([^\"]++)\"                                 "
              "            |                                             "
              "            ([^'\"\\s>]++)                                "
              "        )                                                 "
              ")                                                         ",
              UREGEX_COMMENTS, err_)
    {
        VERIFY(U_SUCCESS(err_) == TRUE);
    }


    /** Заполнение пустых полей в задании (mime-типа и кодировки), исходя
      * из их содержимого. В худшем случае метод ничего не сделает.
      * Метод не потокобезопасен и не генерирует исключения. */
    void FillEmptyFieldsInJob(Manager::ExtractorJob* job)
    {
        if ( !job->mime_type.empty() && !job->charset.empty() ) return;

        // В HTTP-заголовке кодировки не было. Пробуем найти ее в теле HTML.
        // Допустим это HTML или XHTML, попытаемся найти мета-тег.
        // Порядок приоритетности получения кодировки:
        // HTTP-заголовок
        // BOM-документа
        // Meta.
        // см. http://www.validome.org/lang/en/errors/HTML-CHARSET

        ::std::pair<MimeType, Charset> meta = HTMLMetaTagContent(job->data);
        if ( job->mime_type.empty() ) job->mime_type = meta.first;
        if ( job->charset.empty() ) job->charset = meta.second;

        // Если кодировки нигде не было, попробуем ее определить.
        // Из русских кодировок ICU определяет KOI8-R, windows-1251 и любой UTF.
        if ( job->charset.empty() ) job->charset = DetectCharset(job->data);
    }


    /** Определение mime-типа и кодировки из мета-тега html <meta>.
      * В случае, если тег не найден, возвращаются пустые строки.
      * Функция исходит из того, что кодировка однобайтовая.
      * Если это заведомо не так — для определения кодировки следует
      * использовать метод DetectCharset.
      * Метод не потокобезопасен и не генерирует исключения. */
    ::std::pair<MimeType,Charset> HTMLMetaTagContent(
        const ::std::vector<char>& data)
    {
        if ( data.empty() )
            return ::std::pair<MimeType,Charset>();

        // meta состоит только из ASCII-символов.
        // Поэтому декодируем ее простейшим US_INV, который не подгружает
        // преобразователи (т.е. с ним создание строки быстрее).
        // В случае, если в meta будут не ASCII-символы строка останется
        // корректной, хотя и будет содержать некорректные кодовые точки,
        // не влияющие на цель поиска: "Content-Type".
        const icu::UnicodeString t(&data[0], I32(data.size()), US_INV);
        VERIFY(t.isEmpty() == FALSE && t.isBogus() == FALSE);

        meta_content_type_extractor_.reset(t);
        if ( meta_content_type_extractor_.find() == FALSE )
            return make_pair(MimeType(), Charset());

        err_ = U_ZERO_ERROR;
        assert(meta_content_type_extractor_.group(1, err_).isEmpty() +
               meta_content_type_extractor_.group(2, err_).isEmpty() +
               meta_content_type_extractor_.group(3, err_).isEmpty() == 2);

        const icu::UnicodeString u_content_type =
            meta_content_type_extractor_.group(1, err_) +
            meta_content_type_extractor_.group(2, err_) +
            meta_content_type_extractor_.group(3, err_);
        VERIFY(U_SUCCESS(err_) == TRUE);

        // Минимальный корректный HTTP-заголовок с 1 сообщением.
        const ::std::string http_header =
            "HTTP/1.1\r\nContent-Type: " +
            UnicodeString2String(u_content_type, "") +
            "\r\n\r\n";

        HttpHeaderParser http_header_parser(http_header);
        http_header_parser.Parse();

        return ::std::make_pair(http_header_parser.mime_type(),
                                http_header_parser.charset());
    }


    /** Статистическое определение кодировки. При определении игнорирунтся
      * текст между символов '<' и '>', таким образом большинство тегов
      * (если они присутствуют) не повлияют на результат.
      * В случае низкой уверенности возвращается пустая строка.
      * Определяются следующие кодировки:
      * Юникод: UTF-8, UTF-16BE, UTF-16LE, UTF-32BE, UTF-32LE.
      * Русские: Windows-1251, KOI8-R, ISO-8859-5.
      * Shift_JIS, ISO-2022-JP, ISO-2022-CN, ISO-2022-KR, GB18030, Big5, EUC-JP,
      * Korean, ISO-8859-1, ISO-8859-2, ISO-8859-6, ISO-8859-7, ISO-8859-8,
      * windows-1256, ISO-8859-9.
      * Метод не потокобезопасен и не генерирует исключения.
      * @sa http://userguide.icu-project.org/conversion/detection */
    Charset DetectCharset(const ::std::vector<char>& data) const
    {
        err_ = U_ZERO_ERROR;

        UCharsetDetector* csd = ucsdet_open(&err_);
        ucsdet_setText(csd, &data[0], I32(data.size()), &err_);
        ucsdet_enableInputFilter(csd, TRUE);  // игнорирует HTML-разметку

        const UCharsetMatch* ucm = ucsdet_detect(csd, &err_);

        // iu9.bmstu.ru — windows-1251, корректно, 40%.
        // "Привет, мир" в koi8-r 14 раз — koi8-r, 80%.
        // iu-9.info — utf-8, 100%.
        // 10% точно мало, исходя из документации.
        if ( ucsdet_getConfidence(ucm, &err_) < 30 )
        {
            ucsdet_close(csd);
            VERIFY(U_SUCCESS(err_) == TRUE);
            return Charset();
        }

        const char* charset = ucsdet_getName(ucm, &err_);
        VERIFY(U_SUCCESS(err_) == TRUE);

        const Charset result(charset);
        ucsdet_close(csd);
        return result;
    }


    icu::UnicodeString ConvertDataToUnicode(const Manager::ExtractorJob& job) const
    {
        // Игнорируются не alphanum символы и регистр в названии кодировки.
        // Конвертирование может не удасться, из-за плохого имени кодировки
        // или некорректной utf-8 последовательности.
        return icu::UnicodeString(&job.data[0], I32(job.data.size()),
                                  &job.charset[0]);
    }


    /** Выделение ссылок из RSS/atom через внешний XSTL-процессор.
      * В случае успешного выполнения метод возвращает true.
      * В противном случае, метод отправляет сообщение об ошибке менеджеру
      * и возвращает false.
      * Метод не потокобезопасен и не генерирует исключения. */
    bool ExtractFromXml(Manager::ExtractorJob* job)
    {
        ::std::ofstream rss_stream("rss.xml", ::std::ios::binary |
                                              ::std::ios::out |
                                              ::std::ios::trunc);
        if ( !rss_stream.good() )
        {
            manager_->PushFailedExtractorJob(
                *job, "Couldn't create temp rss.xml file");

            rss_stream.close();
            ::std::remove("rss.xml");
            return false;
        }

        const ::std::streamsize size =
            libv::IntegerCast< ::std::streamsize >(job->data.size());

        rss_stream.write(&job->data[0], size);

        if ( !rss_stream.good() )
        {
            manager_->PushFailedExtractorJob(
                *job, "Can't write to temp rss.xml file");

            rss_stream.close();
            ::std::remove("rss.xml");
            return false;
        }

        rss_stream.close();


        // Отменяем валидацию документа, если для этого требуется закрузка
        // данных из Сети. Краулер может работать на машине с брандмауэром,
        // в котором разрешение прописано только ему.
        const char* cmd ="xsltproc --nonet rss2urls.xsl rss.xml > urls.txt";
        if ( ::std::system(cmd) != 0 )
        {
            manager_->PushFailedExtractorJob(
                *job, "XSLT processing of rss failed");

            rss_stream.close();
            ::std::remove("rss.xml");
            ::std::remove("urls.txt");
            return false;
        }
        ::std::remove("rss.xml");  // не проверяем ошибку, она не критична


        ::std::ifstream urls_stream("urls.txt", std::ios::in);
        while ( urls_stream.good() )
        {
            AsciiUrl url;
            ::std::getline(urls_stream, url);

            const UnicodeUrl u_url(url.c_str(), -1, "utf-8");
            if ( u_url.isBogus() == false && u_url.isEmpty() == false )
                job->urls.push_back(u_url);
        }

        if ( !urls_stream.eof() )  // чтение завершилось не из-за EOF
        {
            job->urls.clear();
            manager_->PushFailedExtractorJob(
                *job, "Error while reading urls.txt file");

            urls_stream.close();
            ::std::remove("urls.txt");
            return false;
        }
        urls_stream.close();
        ::std::remove("urls.txt");  // не проверяем ошибку, она не критична

        return true;
    }


    /** Конвертирование всех выделенных URL в Unicode-формат. При возникновении
      * ошибки, некорректный URL удаляется из контейнера job->urls.
      * Метод не потокобезопасен и не генерирует исключения. */
    void ConvertUrlsToUnicode(Manager::ExtractorJob* job)
    {
        // Любой выделенный URL может быть закодированным.
        // Если это так Ascii2Unicode декодирует его.
        // В противном случае, метод не сделает ничего.
        // В случае если путь содержит '%' он будет обрабатываться как
        // процентное кодирование.
        for ( size_t i = 0 ; i < job->urls.size() ; )
        {
            try
            {
                job->urls[i] = url_converter_.Ascii2Unicode(job->urls[i]);
                if ( job->urls[i].isBogus() )
                    HandleConversionError(job, i);
                else
                    i++;
            }
            catch(libv::PrecondException&)  // не смогли выделить части url
            {
                // Удаляет плохой URL и изменяет размер job->urls.
                HandleConversionError(job, i);
            }
            catch(UrlConverterException&)
            {
                HandleConversionError(job, i);
            }
            catch(libv::IntegerCastException& e)
            {
                HandleConversionError(job, i);
            }
            catch(::std::exception& e)
            {
                ERR("ConvertUrlsToUnicode() caught unexpected exception: "
                    << e.what());
                HandleConversionError(job, i);
            }
        }
    }


    /** Обработка ошибки преобразования URL в методе ConvertUrlsToUnicode:
      * отправка сообщения об ошибке менеджеру и удаление некорректного URL из
      * контейнера job->urls.
      * Метод не потокобезопасен и не генерирует исключения. */
    void HandleConversionError(Manager::ExtractorJob* job, const size_t url_id)
    {
        manager_->SendMessage(*job, ::std::string("Bad url found: ") +
                                    UnicodeString2String(job->urls[url_id]));

        // Обязательно сохраняем в отдельную переменную, а не
        // используем ссылку urls[i], значение по которой будет меняться
        // во время выполнения ::std::remove.
        const UAsciiUrl u = job->urls[url_id];
        job->urls.erase(::std::remove(job->urls.begin(), job->urls.end(), u),
                        job->urls.end());
    }



    /** Указатель на менеджер, выдающий задания и принимающий результаты. */
    Manager* manager_;

    /** Объект, выделяющий ссылки из HTML. */
    HtmlUrlExtractor url_extractor_;

    /** Флаг, определяющий выделять ли только ссылки (true)
      * или ссылки и текст (false).*/
    bool extract_text_;

    /** Объект, выделяющий текст из HTML. */
    HtmlTextExtractor text_extractor_;

    /** Переменная, используемая для получения кода ошибки функций ICU. */
    mutable UErrorCode err_;

    /** Регулярное выражение, выделяющее тег <meta>. */
    icu::RegexMatcher meta_content_type_extractor_;

    /** Преобразователь URL из Ascii-формы в Unicode. */
    UrlConverter url_converter_;
};

}  // scrw

#endif  // SCRW_DOWNLOADER_HPP_
